package com.li.collision
{
import away3dlite.arcane;
import away3dlite.core.base.Face;

import away3dlite.core.base.Mesh;

import flash.geom.Matrix3D;
import flash.geom.Rectangle;
import flash.geom.Vector3D;

use namespace arcane;

/*
    Represents a frustum made out of 4 planes instead of six: left, right, top and bottom planes, no near and far planes.
    Can determine if points or faces collide with it.
 */
public class HollowFrustum
{
    // Planes.
    public static const LEFT:int = 0;
    public static const RIGHT:int = 1;
    public static const TOP:int = 2;
    public static const BOTTOM:int = 3;
    public var planes:Vector.<MathematicalPlane>;

    // Clasification.
    public static const OUT:int = 0;
    public static const IN:int = 1;

    // Corner points.
    private var _nblCorner:Vector3D; // near bottom left corner.
    private var _nbrCorner:Vector3D; // near bottom right corner.
    private var _ntlCorner:Vector3D; // near top left corner.
    private var _ntrCorner:Vector3D; // near top right corner.
    private var _fblCorner:Vector3D; // far bottom left corner.
    private var _fbrCorner:Vector3D; // far bottom right corner.
    private var _ftlCorner:Vector3D; // far top left corner.
    private var _ftrCorner:Vector3D; // far top right corner.

    private var _mesh:Mesh;
    private var _vertices:Vector.<Number>;
    private var _v0:Vector3D, _v1:Vector3D, _v2:Vector3D, _v3:Vector3D;
    private var _meshMatrix:Matrix3D;

    public function HollowFrustum()
    {
        // Init planes.
        planes = new Vector.<MathematicalPlane>();
        planes[LEFT] = new MathematicalPlane();
        planes[RIGHT] = new MathematicalPlane();
        planes[TOP] = new MathematicalPlane();
        planes[BOTTOM] = new MathematicalPlane();

        // Init frustum corners.
        _nblCorner = new Vector3D();
        _nbrCorner = new Vector3D();
        _ntlCorner = new Vector3D();
        _ntrCorner = new Vector3D();
        _fblCorner = new Vector3D();
        _fbrCorner = new Vector3D();
        _ftlCorner = new Vector3D();
        _ftrCorner = new Vector3D();
    }

    /*
        When classifying faces from a given mesh, the mesh must be set here
        before running any test on the faces.
     */
    public function set mesh(value:Mesh):void
    {
        _mesh = value;
        _vertices = _mesh.vertices;
    }

    /*
        Determines if a given point in space is inside or outside the frustum.
     */
    public function classifyPoint(point:Vector3D):int
    {
        // All planes have normals pointing into the cube, hence if the point is not
        // found in the front of any plane, it can't be in the cube.
        if(planes[LEFT].classifyPoint(point) != MathematicalPlane.FRONT)
            return HollowFrustum.OUT;
        if(planes[RIGHT].classifyPoint(point) != MathematicalPlane.FRONT)
            return HollowFrustum.OUT;
        if(planes[TOP].classifyPoint(point) != MathematicalPlane.FRONT)
            return HollowFrustum.OUT;
        if(planes[BOTTOM].classifyPoint(point) != MathematicalPlane.FRONT)
            return HollowFrustum.OUT;

        // All tests passed, point must be inside the cube.
        return HollowFrustum.IN;
    }

    /*
        Determines if all the vertices in a face (in scene space) are all on the back of a given plane.
     */
    public function classifyFaceVerticesToFrustumSide(face:Face, plane:MathematicalPlane):int
    {
        // Any of the vertices are in the front of the plane.
        if(plane.classifyPoint(_v0) != MathematicalPlane.BACK)
            return MathematicalPlane.FRONT;
        if(plane.classifyPoint(_v1) != MathematicalPlane.BACK)
            return MathematicalPlane.FRONT;
        if(plane.classifyPoint(_v2) != MathematicalPlane.BACK)
            return MathematicalPlane.FRONT;
        if(face.i3 != 0)
        {
            if(plane.classifyPoint(_v3) != MathematicalPlane.BACK)
                return MathematicalPlane.FRONT;
        }

        // All vertices are on the back of the plane.
        return MathematicalPlane.BACK;
    }

    /*
        Determines if a face touches the inside of the frustum.
     */
    public function classifyFace(face:Face):int
    {
        // First, translate face vertices to scene space.
        _meshMatrix = _mesh.sceneMatrix3D;
        _v0 = _meshMatrix.transformVector(new Vector3D(_vertices[face.u0], _vertices[face.v0], _vertices[face.t0]));
        _v1 = _meshMatrix.transformVector(new Vector3D(_vertices[face.u1], _vertices[face.v1], _vertices[face.t1]));
        _v2 = _meshMatrix.transformVector(new Vector3D(_vertices[face.u2], _vertices[face.v2], _vertices[face.t2]));
        if(face.i3 != 0)
            _v3 = _meshMatrix.transformVector(new Vector3D(_vertices[face.u3], _vertices[face.v3], _vertices[face.t3]));

        // First make an easy test.
        // If any of the face's vertices are inside the frustum, then we have a collision for sure.
        if(classifyPoint(_v0))
            return HollowFrustum.IN;
        if(classifyPoint(_v1))
            return HollowFrustum.IN;
        if(classifyPoint(_v2))
            return HollowFrustum.IN;
        if(face.i3 != 0)
        {
            if(classifyPoint(_v3))
                return HollowFrustum.IN;
        }

        // Next step considers collisions with all vertices of the face outside of the frustum.
        // An example of this could be when only an edge of the face is inside of the frustum, while its vertices lay outside.
        // For this we use a simplified method of separating axis test. We search for a separating axis between the face and the frustum.
        // If all of the vertices of the face lay on the back of any of the planes, then we have a separating axis between the frustum and the face.
        if(classifyFaceVerticesToFrustumSide(face, planes[LEFT]) == MathematicalPlane.BACK)
            return HollowFrustum.OUT;
        if(classifyFaceVerticesToFrustumSide(face, planes[RIGHT]) == MathematicalPlane.BACK)
            return HollowFrustum.OUT;
        if(classifyFaceVerticesToFrustumSide(face, planes[TOP]) == MathematicalPlane.BACK)
            return HollowFrustum.OUT;
        if(classifyFaceVerticesToFrustumSide(face, planes[BOTTOM]) == MathematicalPlane.BACK)
            return HollowFrustum.OUT;

        // No separating axis has been found in previous test, a collision must occur.
        return HollowFrustum.IN;
    }

    /*
        Defines the frustum as a rectangle that projects from a given view point.
        Imagine it as a virtual camera with a projection plane in front of it... A box on the projection plane
        is projected, as seen from that virtual camera, into the scene.
     */
    public function defineFromViewPointBoundingBox(box:Rectangle, viewPointTransform:Matrix3D, focalLength:Number):void
    {
        // TODO: I'm sure this definition could be done much faster with matrices.

        // Use the box to construct to front side of the frustum in 3D space.
        // At this stage, the front (near) corners are considered to be in camera space
        // and laying in the virtual view plane of the camera.
        _nblCorner.x = box.left;
        _nblCorner.y = box.bottomRight.y;
        _nblCorner.z = focalLength;
        _nbrCorner.x = box.right;
        _nbrCorner.y = box.bottomRight.y;
        _nbrCorner.z = focalLength;
        _ntlCorner.x = box.left;
        _ntlCorner.y = box.topLeft.y;
        _ntlCorner.z = focalLength;
        _ntrCorner.x = box.right;
        _ntrCorner.y = box.topLeft.y;
        _ntrCorner.z = focalLength;

        // Now, to translate these front corners into actual scene space coordinates,
        // the camera matrix is used to transform them.
        _nblCorner = viewPointTransform.transformVector(_nblCorner);
        _nbrCorner = viewPointTransform.transformVector(_nbrCorner);
        _ntlCorner = viewPointTransform.transformVector(_ntlCorner);
        _ntrCorner = viewPointTransform.transformVector(_ntrCorner);

        // Then, we compare the position of the camera and front corners that were just constructed
        // and for each corner, we produce the corresponding corner of the far side of the frustum.
        // How deep the far corners will be is irrelevant, since the only point of the corners is
        // to use them to construct infinite planes.
        _fblCorner = _nblCorner.add(_nblCorner.subtract(viewPointTransform.position));
        _fbrCorner = _nbrCorner.add(_nbrCorner.subtract(viewPointTransform.position));
        _ftlCorner = _ntlCorner.add(_ntlCorner.subtract(viewPointTransform.position));
        _ftrCorner = _ntrCorner.add(_ntrCorner.subtract(viewPointTransform.position));

        // Once the corners are calculated, its simply a matter of constructing the planes by properly grouping them.
        planes[LEFT].defineFrom3points(_nblCorner, _fblCorner, _ftlCorner);
        planes[RIGHT].defineFrom3points(_ntrCorner, _ftrCorner, _fbrCorner);
        planes[TOP].defineFrom3points(_ntlCorner, _ftlCorner, _ftrCorner);
        planes[BOTTOM].defineFrom3points(_nbrCorner, _fbrCorner, _fblCorner);
    }

    public function getFrustumCorners():Array
    {
        var corners:Array = [_nblCorner, _nbrCorner, _ntlCorner, _ntrCorner, _fblCorner, _fbrCorner, _ftlCorner, _ftrCorner];
        return corners;
    }

    public function toString():String
    {
        return "HollowFrustum - LEFT: " + planes[LEFT] + ", RIGHT: " + planes[RIGHT] + ", BOTTOM: " + planes[BOTTOM] + ", TOP: " + planes[TOP];
    }
}
}
